#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# PROFIBUS DP - LinuxCNC HAL module
#
# Copyright 2016-2021 Michael Buesch <m@bues.ch>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#

from __future__ import division, absolute_import, print_function, unicode_literals

import sys
import os
import time
import getopt
import struct

from pyprofibus import ProfibusError, FdlError, DpError
from pyprofibus.compat import isPy2Compat
from pyprofibus.util import fileExists, monotonic_time
from pyprofibus.dp import DpTelegram_SetPrm_Req
from pyprofibus.conf import PbConf, PbConfError
from pyprofibus.version import *


class SigBit(object):
	def __init__(self, hal, halName, byteOffset, bitOffset):
		self.hal = hal
		self.halName = halName
		self.byteOffset = byteOffset
		self.bitOffset = bitOffset
		self.setMask = 1 << bitOffset
		self.clrMask = ~(1 << bitOffset)

	def fromHal(self, destBuf):
		if self.hal[self.halName]:
			destBuf[self.byteOffset] |= self.setMask
		else:
			destBuf[self.byteOffset] &= self.clrMask

	def toHal(self, srcBuf):
		self.hal[self.halName] = (srcBuf[self.byteOffset] >> self.bitOffset) & 1

	def __str__(self):
		return "profibus.%s" % self.halName

class SigU8(object):
	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		destBuf[self.offset] = self.hal[self.halName] & 0xFF

	def toHal(self, srcBuf):
		self.hal[self.halName] = srcBuf[self.offset] & 0xFF

	def __str__(self):
		return "profibus.%s" % self.halName

class SigU16(object):
	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		word = self.hal[self.halName] & 0xFFFF
		destBuf[self.offset] = (word >> 8) & 0xFF
		destBuf[self.offset + 1] = word & 0xFF

	def toHal(self, srcBuf):
		word = (srcBuf[self.offset] << 8) |\
		       srcBuf[self.offset + 1]
		self.hal[self.halName] = word & 0xFFFF

	def __str__(self):
		return "profibus.%s" % self.halName

class SigS16(object):
	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		word = self.hal[self.halName] & 0xFFFF
		destBuf[self.offset] = (word >> 8) & 0xFF
		destBuf[self.offset + 1] = word & 0xFF

	def toHal(self, srcBuf):
		word = (srcBuf[self.offset] << 8) |\
		       srcBuf[self.offset + 1]
		if word & 0x8000:
			self.hal[self.halName] = -((~word + 1) & 0xFFFF)
		else:
			self.hal[self.halName] = word & 0xFFFF

	def __str__(self):
		return "profibus.%s" % self.halName

class SigU31(object):
	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		dword = self.hal[self.halName] & 0x7FFFFFFF
		destBuf[self.offset] = (dword >> 24) & 0xFF
		destBuf[self.offset + 1] = (dword >> 16) & 0xFF
		destBuf[self.offset + 2] = (dword >> 8) & 0xFF
		destBuf[self.offset + 3] = dword & 0xFF

	def toHal(self, srcBuf):
		dword = (srcBuf[self.offset] << 24) |\
		        (srcBuf[self.offset + 1] << 16) |\
		        (srcBuf[self.offset + 2] << 8) |\
		        srcBuf[self.offset + 3]
		self.hal[self.halName] = dword & 0x7FFFFFFF

	def __str__(self):
		return "profibus.%s" % self.halName

class SigS32(object):
	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		dword = self.hal[self.halName] & 0xFFFFFFFF
		destBuf[self.offset] = (dword >> 24) & 0xFF
		destBuf[self.offset + 1] = (dword >> 16) & 0xFF
		destBuf[self.offset + 2] = (dword >> 8) & 0xFF
		destBuf[self.offset + 3] = dword & 0xFF

	def toHal(self, srcBuf):
		dword = (srcBuf[self.offset] << 24) |\
		        (srcBuf[self.offset + 1] << 16) |\
		        (srcBuf[self.offset + 2] << 8) |\
		        srcBuf[self.offset + 3]
		if dword & 0x80000000:
			self.hal[self.halName] = -((~dword + 1) & 0xFFFFFFFF)
		else:
			self.hal[self.halName] = dword & 0xFFFFFFFF

	def __str__(self):
		return "profibus.%s" % self.halName

class SigFloat(object):
	floatStruct = struct.Struct(str('>f'))

	def __init__(self, hal, halName, offset):
		self.hal = hal
		self.halName = halName
		self.offset = offset

	def fromHal(self, destBuf):
		buf = self.floatStruct.pack(self.hal[self.halName])
		if isPy2Compat:
			buf = [ ord(b) for b in buf ]
		destBuf[self.offset : self.offset + 4] = buf[0 : 4]

	def toHal(self, srcBuf):
		dword = (srcBuf[self.offset] << 24) |\
		        (srcBuf[self.offset + 1] << 16) |\
		        (srcBuf[self.offset + 2] << 8) |\
		        srcBuf[self.offset + 3]
		if isPy2Compat:
			value = self.floatStruct.unpack(
				chr((dword >> 24) & 0xFF) +\
				chr((dword >> 16) & 0xFF) +\
				chr((dword >> 8) & 0xFF) +\
				chr(dword & 0xFF)
			)[0]
		else:
			value = self.floatStruct.unpack(
				bytes( ((dword >> 24) & 0xFF,
					(dword >> 16) & 0xFF,
					(dword >> 8) & 0xFF,
					dword & 0xFF)
				)
			)[0]
		self.hal[self.halName] = value

	def __str__(self):
		return "profibus.%s" % self.halName

class Worker(object):
	def __init__(self, hal, master):
		self.__configDone = False
		self.hal = hal
		self.master = master
		self.slaves = master.getSlaveList()

	def __buildTable(self, slaveAddr, direction, size):
		tab = []
		for i in range(0, size):
			for bitNr in range(8):
				halName = "slave.%d.%s.bit.%d.%d" % (
					slaveAddr, direction, i, bitNr)
				if self.hal[halName + ".active"]:
					tab.append(SigBit(self.hal, halName,
							  i, bitNr))
			halName = "slave.%d.%s.u8.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigU8(self.hal, halName, i))
			if i % 2:
				continue
			if size - i < 2:
				continue

			halName = "slave.%d.%s.u16.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigU16(self.hal, halName, i))
			halName = "slave.%d.%s.s16.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigS16(self.hal, halName, i))
			if size - i < 4:
				continue

			halName = "slave.%d.%s.u31.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigU31(self.hal, halName, i))
			halName = "slave.%d.%s.s32.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigS32(self.hal, halName, i))
			halName = "slave.%d.%s.float.%d" % (
				slaveAddr, direction, i)
			if self.hal[halName + ".active"]:
				tab.append(SigFloat(self.hal, halName, i))
		return tab

	def __tryBuildConfig(self):
		if not self.hal["config.ready"]:
			return

		for slave in self.slaves:
			slaveConf = slave.slaveConf
			if slaveConf is None:
				continue

			activePbMasterOutputs = self.__buildTable(
				slave.slaveAddr, "input", slaveConf.inputSize)

			activePbMasterInputs = self.__buildTable(
				slave.slaveAddr, "output", slaveConf.outputSize)

			slave.userData["activePbMasterInputs"] = activePbMasterInputs
			slave.userData["activePbMasterOutputs"] = activePbMasterOutputs

			printInfo("Active DP slave (addr=%d) pins:" % slave.slaveAddr)
			for sig in activePbMasterOutputs:
				printInfo("DP slave input:   " + str(sig))
			for sig in activePbMasterInputs:
				printInfo("DP slave output:  " + str(sig))

		self.__configDone = True
		printInfo("HAL configuration done")

	def mainLoop(self):
		master = self.master
		while watchdog() and not self.__configDone:
			self.__tryBuildConfig()
			time.sleep(0.1)
		while watchdog():
			for slave in self.slaves:
				slaveConf = slave.slaveConf
				if slaveConf is not None:
					txData = bytearray(slaveConf.inputSize)
					for sig in slave.userData["activePbMasterOutputs"]:
						sig.fromHal(txData)
					slave.setMasterOutData(txData)
			slave = master.run()
			if slave:
				slaveConf = slave.slaveConf
				if slaveConf is not None:
					rxData = slave.getMasterInData()
					if rxData:
						slaveOutputSize = slaveConf.outputSize
						if len(rxData) > slaveOutputSize:
							rxData = rxData[0 : slaveOutputSize]
						if len(rxData) < slaveOutputSize:
							rxData += b'\0' * (slaveOutputSize - len(rxData))
						for sig in slave.userData["activePbMasterInputs"]:
							sig.toHal(rxData)

class LinuxCNC_NotRunning(Exception):
	pass

def printError(msg):
	sys.stderr.write("pyprofibus: " + msg + "\n")

def printWarning(msg):
	sys.stderr.write("pyprofibus: " + msg + "\n")

def printInfo(msg):
	sys.stdout.write("pyprofibus: " + msg + "\n")

# Check presence of LinuxCNC.
# Returns normally, if LinuxCNC is detected.
# Raises LinuxCNC_NotRunning, if LinuxCNC is not detected.
def watchdog():
	# Check whether LinuxCNC is running.
	for lockname in ("/tmp/linuxcnc.lock", "/tmp/emc.lock"):
		if fileExists(lockname):
			return True
	if not opt_watchdog:
		# The check is disabled. Return success.
		return True
	printError("LinuxCNC doesn't seem to be running. "\
		   "(Use '--watchdog off' to disable this check.)")
	raise LinuxCNC_NotRunning()

# Create the LinuxCNC HAL pins
def createHalPins(hal, slaveAddr, slaveOutputSize, slaveInputSize):
	HAL_BIT, HAL_U32, HAL_S32, HAL_FLOAT = \
		LinuxCNC_HAL.HAL_BIT, LinuxCNC_HAL.HAL_U32, \
		LinuxCNC_HAL.HAL_S32, LinuxCNC_HAL.HAL_FLOAT
	HAL_IN, HAL_OUT, HAL_RO, HAL_RW = \
		LinuxCNC_HAL.HAL_IN, LinuxCNC_HAL.HAL_OUT, \
		LinuxCNC_HAL.HAL_RO, LinuxCNC_HAL.HAL_RW
	addr = slaveAddr

	printInfo("DP slave %d output: %d bytes" % (addr, slaveOutputSize))
	printInfo("DP slave %d input:  %d bytes" % (addr, slaveInputSize))

	# Create the input pins
	for i in range(slaveInputSize):
		for bit in range(8):
			hal.newpin("slave.%d.input.bit.%d.%d" % (addr, i, bit),
				   HAL_BIT, HAL_IN)
			hal.newparam("slave.%d.input.bit.%d.%d.active" % (addr, i, bit),
				     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.input.u8.%d" % (addr, i),
			   HAL_U32, HAL_IN)
		hal.newparam("slave.%d.input.u8.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		if i % 2:
			continue
		if slaveInputSize - i < 2:
			continue
		hal.newpin("slave.%d.input.u16.%d" % (addr, i),
			   HAL_U32, HAL_IN)
		hal.newparam("slave.%d.input.u16.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.input.s16.%d" % (addr, i),
			   HAL_S32, HAL_IN)
		hal.newparam("slave.%d.input.s16.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		if slaveInputSize - i < 4:
			continue
		hal.newpin("slave.%d.input.u31.%d" % (addr, i),
			   HAL_U32, HAL_IN)
		hal.newparam("slave.%d.input.u31.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.input.s32.%d" % (addr, i),
			   HAL_S32, HAL_IN)
		hal.newparam("slave.%d.input.s32.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.input.float.%d" % (addr, i),
			   HAL_FLOAT, HAL_IN)
		hal.newparam("slave.%d.input.float.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)

	# Create the output pins
	for i in range(slaveOutputSize):
		for bit in range(8):
			hal.newpin("slave.%d.output.bit.%d.%d" % (addr, i, bit),
				   HAL_BIT, HAL_OUT)
			hal.newparam("slave.%d.output.bit.%d.%d.active" % (addr, i, bit),
				     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.output.u8.%d" % (addr, i),
			   HAL_U32, HAL_OUT)
		hal.newparam("slave.%d.output.u8.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		if i % 2:
			continue
		if slaveOutputSize < 2:
			continue
		hal.newpin("slave.%d.output.u16.%d" % (addr, i),
			   HAL_U32, HAL_OUT)
		hal.newparam("slave.%d.output.u16.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.output.s16.%d" % (addr, i),
			   HAL_S32, HAL_OUT)
		hal.newparam("slave.%d.output.s16.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		if slaveOutputSize < 4:
			continue
		hal.newpin("slave.%d.output.u31.%d" % (addr, i),
			   HAL_U32, HAL_OUT)
		hal.newparam("slave.%d.output.u31.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.output.s32.%d" % (addr, i),
			   HAL_S32, HAL_OUT)
		hal.newparam("slave.%d.output.s32.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)
		hal.newpin("slave.%d.output.float.%d" % (addr, i),
			   HAL_FLOAT, HAL_OUT)
		hal.newparam("slave.%d.output.float.%d.active" % (addr, i),
			     HAL_BIT, HAL_RW)

	hal.newparam("config.ready", HAL_BIT, HAL_RW)

def usage():
	print("pyprofibus-linuxcnc-hal version %s" % VERSION_STRING)
	print("")
	print("Usage: pyprofibus-linuxcnc-hal [OPTIONS] pyprofibus.conf")
	print("")
	print("Options:")
	print("")
	print(" -L|--loglevel LVL       Set the log level:")
	print("                         0: Log nothing")
	print("                         1: Log errors")
	print("                         2: Log errors and warnings")
	print("                         3: Log errors, warnings and info messages (default)")
	print("                         4: Verbose logging")
	print("                         5: Extremely verbose logging")
	print(" -N|--nice NICE          Renice the process. -20 <= NICE <= 19.")
	print("                         Default: Do not renice")
	print("")
	print("Debugging options:")
	print(" -W|--watchdog 1/0       Enable/disable LinuxCNC runtime watchdog.")
	print("                         Default: on")
	print("")
	print("For an example LinuxCNC HAL configuration see:")
	print("  linuxcnc-demo.hal")

def main():
	global LinuxCNC_HAL

	global opt_loglevel
	global opt_nice
	global opt_watchdog

	opt_loglevel = 3
	opt_nice = None
	opt_watchdog = True

	try:
		(opts, args) = getopt.getopt(sys.argv[1:],
			"hL:N:W:",
			[ "help",
			  "loglevel=",
			  "nice=",
			  "watchdog=", ])
	except getopt.GetoptError as e:
		printError(str(e))
		usage()
		return 1
	for (o, v) in opts:
		if o in ("-h", "--help"):
			usage()
			return 0
		if o in ("-L", "--loglevel"):
			try:
				opt_loglevel = int(v)
			except ValueError:
				printError("-L|--loglevel: Invalid log level")
				return 1
		if o in ("-N", "--nice"):
			try:
				opt_nice = int(v)
				if opt_nice < -20 or opt_nice > 19:
					raise ValueError
			except ValueError:
				printError("-N|--nice: Invalid niceness level")
				return 1
		if o in ("-W", "--watchdog"):
			opt_watchdog = str2bool(v)
	if len(args) != 1:
		usage()
		return 1
	configFile = args[0]

	result = 0
	try:
		# Parse the Profibus config file
		config = PbConf.fromFile(configFile)
		if opt_loglevel >= 4 and config.debug < 1:
			config.debug = 1

		# Adjust process priority
		if opt_nice is not None:
			try:
				os.nice(opt_nice)
			except OSError as e:
				printError("Failed to renice process to "
					   "%d: %s" % (opt_nice, str(e)))
				return 1

		# Try to import the LinuxCNC HAL module
		try:
			import hal as LinuxCNC_HAL
		except ImportError as e:
			printError("Failed to import LinuxCNC HAL "
				   "module: %s" % str(e))
			return 1

		# Create the LinuxCNC HAL component.
		hal = LinuxCNC_HAL.component("profibus")

		# Create the HAL pins.
		for slaveConf in config.slaveConfs:
			createHalPins(hal=hal,
				      slaveAddr=slaveConf.addr,
				      slaveInputSize=slaveConf.inputSize,
				      slaveOutputSize=slaveConf.outputSize)

		# Setup the PROFIBUS stack.
		master = config.makeDPM()
		for slaveConf in config.slaveConfs:
			slaveDesc = slaveConf.makeDpSlaveDesc()
			dp1PrmMask = bytearray((
				DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE,
				DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG,
				0x00))
			dp1PrmSet  = bytearray((
				DpTelegram_SetPrm_Req.DPV1PRM0_FAILSAFE,
				DpTelegram_SetPrm_Req.DPV1PRM1_REDCFG,
				0x00))
			slaveDesc.setUserPrmData(
				slaveConf.gsd.getUserPrmData(dp1PrmMask=dp1PrmMask,
							     dp1PrmSet=dp1PrmSet))
			master.addSlave(slaveDesc)

		printInfo("Running PROFIBUS-DP master...")
		master.initialize()
		worker = Worker(hal, master)
		hal.ready()
		printInfo("ready.")
		lastExceptionTime = None
		while True:
			try:
				worker.mainLoop()
			except ProfibusError as e:
				now = monotonic_time()
				if lastExceptionTime is not None and\
				   now - lastExceptionTime < 1.0:
					# The last fault is less than one second
					# in the past. Raise a fatal exception.
					printError("Fatal fault detected")
					raise e
				else:
					lastExceptionTime = now
					# Non-fatal fault.
					printError("PROFIBUS fault:\n%s" % str(e))
	except LinuxCNC_NotRunning as e:
		result = 1
	except KeyboardInterrupt as e:
		result = 1
	except PbConfError as e:
		printError("Profibus configuration error:\n%s" % str(e))
		result = 1
	except ProfibusError as e:
		printError("Fatal PROFIBUS fault:\n%s" % str(e))
		result = 1
	printInfo("LinuxCNC HAL module shutdown.")

	return result

if __name__ == "__main__":
	sys.exit(main())
